﻿using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.IO;
using SimpleX.Middlewares;
using System.IO;
using UAParser;

//说明:RequestMiddleware 全局请求/返回 相关代码参考WatchDog
//感谢https://github.com/IzyPro/WatchDog

namespace SimpleX
{
	/// <summary>
	/// RequestMiddleware一个中间件 包含了 全局错误管理,全局请求/返回 记录
	/// </summary>
	public class RequestMiddleware
	{
		private readonly RequestDelegate _next;
		private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
		private readonly ILogger<RequestMiddleware> _logger;

		public RequestMiddleware(RequestDelegate next, ILogger<RequestMiddleware> logger)
		{
			_next = next;
			_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
			_logger = logger;
		}

		public async Task Invoke(HttpContext context)
		{
			//是否启用http日志
			var enableLog = App.Get<bool>("RequestMiddlewareHttpLog");
			var ignoreLog = context.GetMetadata<IgnoreLogAttribute>() != null;
			enableLog = enableLog && !ignoreLog;

			//处理请求和相应
			var requestLog = await LogRequest(context, enableLog);
			var responseLog = await LogResponse(context, requestLog, enableLog);

			//不启用不记录http日志
			if (!enableLog)
				return;

			var timeSpent = responseLog.FinishTime.Subtract(requestLog.StartTime);
			var clientInfo = requestLog.ClientInfo;

			var fullModel = new FullModel
			{
				OpIp = requestLog.IpAddress,
				Name = requestLog.RequestName,
				ReqMethod = requestLog.CodeMethodName,
				ReqHeaders = requestLog.RequestHeaders,
				ReqUrl = requestLog.Path,
				ParamJson = requestLog.RequestBody,
				ResultJson = responseLog.ResponseBody,
				ResultHeaders = responseLog.ResponseHeaders,
				TimeSpentStr = string.Format("{0:D1} hrs {1:D1} mins {2:D1} secs {3:D1} ms", timeSpent.Hours, timeSpent.Minutes, timeSpent.Seconds, timeSpent.Milliseconds),
				TimeSpent = timeSpent.TotalMilliseconds,
				ResponseStatus = responseLog.ResponseStatus,
				OpTime = requestLog.StartTime,
				OpFinishTime = responseLog.FinishTime,
				OpAccount = requestLog.Account,
				OpUser = requestLog.UserName,
				OpUserId = requestLog.UserId,
				MethodName = requestLog.CodeMethodName,
				ClassName = requestLog.ClassName,
				Category = "登录/登出/操作/异常",
				ExeMessage = "异常信息",
				Exception = responseLog.Exception,
				OpAddress = IPHelper.GetAddress(requestLog.IpAddress),
				OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
				OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
			};

			await WriteToDb(fullModel);
		}

		private async Task WriteToDb(FullModel fullModel)
		{
			try
			{
				#region Process Field

				if (fullModel.Exception != null)
					fullModel.ExeMessage = $"Message:{fullModel.Exception?.Message}\nStackTrace:{fullModel.Exception?.StackTrace}";
				else
					fullModel.ExeMessage = null;

				if (fullModel.Exception == null)
				{
					if (fullModel.Name == "LoginIn")
						fullModel.Category = "LOGIN";
					else if (fullModel.Name == "LoginOut")
						fullModel.Category = "LOGOUT";
					else
						fullModel.Category = "OPERATE";
				}
				else
					fullModel.Category = "EXCEPTION";

				#endregion

				var connection = DbContext.Db.GetConnection(SqlsugarConst.DB_Default);
				await connection.Insertable(fullModel).ExecuteCommandAsync();
			}
			catch
			{
			}
		}

		private async Task<RequestModel> LogRequest(HttpContext context, bool enableLog)
		{
			var startTime = DateTime.Now;

			var requestBodyDto = new RequestModel()
			{
				RequestBody = string.Empty,
				//Host = context.Request.Host.ToString(),
				IpAddress = context.Connection.RemoteIpAddress?.ToString(),
				Path = context.Request.Path.ToString(),
				CodeMethodName = context.Request.Method.ToString(),
				QueryString = context.Request.QueryString.ToString(),
				StartTime = startTime,
				RequestHeaders = context.Request.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b),
				UserId = context?.User.FindFirst(ClaimConst.UserId)?.Value,
				UserName = context?.User.FindFirst(ClaimConst.Name)?.Value,
				Account = context?.User.FindFirst(ClaimConst.Account)?.Value,
				RequestName = context.GetMetadata<ActionPermissionAttribute>()?.Name,
			};

			if (!string.IsNullOrWhiteSpace(requestBodyDto.IpAddress))
				requestBodyDto.IpAddress = requestBodyDto.IpAddress.Replace("::ffff:", "");

			if (requestBodyDto.IpAddress == "::1")
				requestBodyDto.IpAddress = "127.0.0.1";

			if (context.Request.Headers.TryGetValue("User-Agent", out var userAgent))
			{
				var clientInfo = Parser.GetDefault().Parse(userAgent);
				requestBodyDto.ClientInfo = clientInfo;
			}

			var endpoint = context.GetEndpoint();
			var actionDescriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();

			if (actionDescriptor != null)
			{
				var methodInfo = actionDescriptor.MethodInfo;
				requestBodyDto.ClassName = methodInfo.DeclaringType?.FullName;
				requestBodyDto.CodeMethodName = methodInfo.Name;
			}

			if (context.Request.ContentLength > 1 && enableLog)
			{
				context.Request.EnableBuffering();
				await using var requestStream = _recyclableMemoryStreamManager.GetStream();
				await context.Request.Body.CopyToAsync(requestStream);
				requestBodyDto.RequestBody = StreamHelper.ReadStreamInChunks(requestStream);
				context.Request.Body.Position = 0;
			}
			return requestBodyDto;
		}

		private async Task<ResponseModel> LogResponse(HttpContext context, RequestModel requestModel, bool enableLog)
		{
			if (!enableLog)
			{
				await Next(context, requestModel);
				return default;
			}

			using (var originalBodyStream = context.Response.Body)
			{
				try
				{
					using (var originalResponseBody = _recyclableMemoryStreamManager.GetStream())
					{
						var responseBodyDto = new ResponseModel();

						context.Response.Body = originalResponseBody;

						await Next(context, requestModel, ex =>
						{
							responseBodyDto.Exception = ex;
						});

						context.Response.Body.Seek(0, SeekOrigin.Begin);
						var responseBody = await new StreamReader(context.Response.Body).ReadToEndAsync();
						context.Response.Body.Seek(0, SeekOrigin.Begin);

						responseBodyDto.ResponseStatus = context.Response.StatusCode;
						responseBodyDto.FinishTime = DateTime.Now;
						responseBodyDto.ResponseHeaders = context.Response.Headers.ContentLength > 0 ? context.Response.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b) : string.Empty;

						//如果是文件只记录文件名
						if (context.Response.Headers.ContainsKey("Content-Disposition"))
							responseBodyDto.ResponseBody = context.Response.Headers.ContentDisposition.ToString();
						else
							responseBodyDto.ResponseBody = responseBody;

						await originalResponseBody.CopyToAsync(originalBodyStream);
						return responseBodyDto;
					}
				}
				catch (OutOfMemoryException ex)
				{
					return new ResponseModel
					{
						ResponseBody = $"Read Response Body Error:{ex.Message}",
						ResponseStatus = context.Response.StatusCode,
						FinishTime = DateTime.Now,
						ResponseHeaders = context.Response.Headers.ContentLength > 0 ? context.Response.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b) : string.Empty,
					};
				}
				finally
				{
					context.Response.Body = originalBodyStream;
				}
			}
		}

		private async Task Next(HttpContext context, RequestModel requestModel, Action<Exception> onError = null)
		{
			try
			{
				await _next(context);
			}
			catch (Exception ex)
			{
				onError?.Invoke(ex);

				context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
				context.Response.ContentType = "application/json";

				var error = "InternalServerError";

				if (App.Configuration.GetValue<bool>("SystemConfig:ShowInternalServerError"))
				{
					error = ex.Message;
				}

				//记录接口耗时
				//_ = long.TryParse(context.Items["__StopwatchMiddlewareWithEndpointExceptioned"]?.ToString(), out long elapsedMilliseconds);

				var json = new UnifyResult<string>()
				{
					Code = HttpStatusCode.InternalServerError,
					Exts = Unify.Read(),
					Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
					Msg = error,
					ElapsedMilliseconds = (long)(DateTime.Now - requestModel.StartTime).TotalMilliseconds
				}.ToJson();

				await context.Response.WriteAsync(json);

				_logger.LogError(exception: ex, message: $"Request To Json: {requestModel.ToJson()}");
			}
		}
	}
}